CloudWatch Syntheticsのsyn-nodejs-puppeteer-7.0をsyn-nodejs-puppeteer-9.0にアップグレードしてみた

CloudWatch Syntheticsのsyn-nodejs-puppeteer-7.0をsyn-nodejs-puppeteer-9.0にアップグレードしてみた

Clock Icon2024.09.17

こんにちは。たかやまです。

2024/3/11にCloudWatch Syntheticsのsyn-nodejs-puppeteer-7.0がリリースされていましたが、ドキュメントを確認するとsyn-nodejs-puppeteer-9.0がリリースされていることがわかりました。
(syn-python-seleniumも4.0がリリースされています)

https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html

コンソールでもアップグレード情報を確認できます。

01-ランタイムアップグレード情報

今回はCloudWatch Syntheticsをsyn-nodejs-puppeteer-7.0 -> syn-nodejs-puppeteer-9.0 にアップグレードする機会があったので、その内容を共有します。

過去のアップグレードに関する記事もあるので、参考にしてください。

https://dev.classmethod.jp/articles/synthetics-node-js-runtime-version-5/

さきにまとめ

以下のいずれかの対応でアップグレードを行う

  1. syn-nodejs-puppeteer-7.0 -> syn-nodejs-puppeteer-9.0への依存関係を解消しアップグレードを行う
    • AWS SDK for JavaScript V2 を利用している方は AWS SDK for JavaScript V3へ移行を行う
    • Puppeteer-core version 21.9.0 -> Puppeteer-core version 22.12.1
  2. 依存関係を含むパッケージングしたZipファイルを用意しアップグレードを行う
    • ただし、Lambdaレイヤーのデプロイパッケージサイズのクォーターに影響されるため、Syntheticsのパッケージと合わせて合計サイズ250MBに収める必要がある

やってみる

syn-nodejs-puppeteer-7.0 -> syn-nodejs-puppeteer-9.0への依存関係を解消しアップグレードする

syn-nodejs-puppeteer-7.0 から syn-nodejs-puppeteer-9.0 にアップグレードに伴う主な依存関係の変更は以下の通りです。

主な依存関係 syn-nodejs-puppeteer-7.0 syn-nodejs-puppeteer-9.0
Lambda Runtime Node.js 18.x Node.js 20.x
Puppeteer-core version 21.9.0 22.12.1
Chromium version 125.0.6422.112 126.0.6478.126

Lambda Runtime の変更

Lambda Runtime については、Node.js 18.x から Node.js 20.x に変更されています。

以下の記載にある通りNode.js 18.x 以降のランタイムはAWS SDK for JavaScript V3へ移行する必要があります。Node.js 18.x ではv2とv3の両方を利用することができましたが、後述する移行検証の中でもわかるように Node.js 20.x ではv2がサポートされなくなっているため、syn-nodejs-puppeteer-9.0を利用する場合にはv3への移行を検討してください。

重要
Lambda Node.js 18 以降のランタイムは AWS SDK for JavaScript V3 を使用しています。以前のランタイムから関数を移行する必要がある場合は、GitHub の「aws-sdk-js-v3 Migration Workshop」の手順に従ってください。AWS SDK for JavaScript バージョン 3 の詳細については、このブログ記事を参照してください。

https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html

v3への移行コストがかかりそうな場合には、2つ目の依存関係を含むパッケージングしたZipファイルを用意しアップグレードを行う方法があるのでそちらを検討してみてください。

Puppeteer-core version の変更

Puppeteer-core についてはメジャーバージョンが21から22に上がっているため、BREAKING CHANGES(破壊的変更)を確認して対応が必要なものは対応します。

⚠ BREAKING CHANGES

  • rename createIncognitoBrowserContext to createBrowserContext (#11834)
  • enable the new-headless mode by default (#11815)
  • remove networkConditions in favor of PredefinedNetworkConditions (#11806)
  • use ReadableStreams (#11805)
  • remove duplicate type names (#11803)
  • remove add/removeEventListener in favor of on/off (#11792)
  • make console warn level compatible with WebDriver BiDi (#11790)
  • remove InterceptResolutionStrategy (#11788)
  • remove devices in favor of KnownDevices (#11787)
  • remove $x and waitForXpath (#11782)
  • remove waitForTimeout (#11780)
  • generate accessible PDFs by default (#11778)
  • remove error const, change CustomError to PuppeteerError (#11777)
  • remove viewport resizing from ElementHandle.screenshot (#11774)
  • remove PUPPETEER_DOWNLOAD_PATH in favor of PUPPETEER_CACHE_DIR (#11605)
  • BiDi cookies (#11532)
  • drop support for node16 (#10912)

https://github.com/puppeteer/puppeteer/releases/tag/puppeteer-core-v22.0.0

アップグレードしてみる

syn-nodejs-puppeteer-7.0 で以下のようなスクリプトを実行していたとします。

const synthetics = require('Synthetics');
const log = require('SyntheticsLogger');
const syntheticsConfiguration = synthetics.getConfiguration();

const flowBuilderBlueprint = async function () {
  // 環境変数の設定
  const url = process.env.URL;
  const region = process.env.REGION;
  const secretName = process.env.SECRETNAME;

  // Synthetics の設定
  syntheticsConfiguration.setConfig({
    includeRequestHeaders: true,
    includeResponseHeaders: true,
    restrictedHeaders: [],
    restrictedUrlParameters: [],
  });

  // AWS Secrets Manager からシークレットを取得
  const AWS = require('aws-sdk');
  const client = new AWS.SecretsManager({ region: region });
  const secret = await client.getSecretValue({ SecretId: secretName }).promise();

  // シークレットの解析
  const secretObj = JSON.parse(secret.SecretString);
  const basicUsername = secretObj.basicUsername;
  const basicPassword = secretObj.basicPassword;
  const portalUserId = secretObj.portalUserId;
  const portalPassword = secretObj.portalPassword;

  let page = await synthetics.getPage();

  // Basic 認証の設定
  await page.setExtraHTTPHeaders({
    Authorization: `Basic ${Buffer.from(`${basicUsername}:${basicPassword}`).toString('base64')}`,
  });

  // 初期URLへ遷移
  await synthetics.executeStep('navigateToUrl', async function (timeoutInMillis = 30000) {
    await page.goto(url, { waitUntil: ['load', 'networkidle0'], timeout: timeoutInMillis });
  });

  // ポータルのログインプロセス
  await synthetics.executeStep('inputPortalUsername', async function () {
    await page.type("[type='text']", portalUserId);
  });

  await synthetics.executeStep('inputPortalPassword', async function () {
    await page.type("[type='password']", portalPassword);
  });

  await synthetics.executeStep('submitPortalLogin', async function () {
    await page.waitForSelector("[type='submit']", { timeout: 30000 });
    await page.click("[type='submit']");
  });

  await synthetics.executeStep('verifyPortalLogin', async function () {
    await page.waitForXPath("//h1[contains(text(), 'Welcome')]", { timeout: 30000 });
  });

  // ログアウトプロセス
  const logoutUrl = 'https://example.com/home';
  await synthetics.executeStep('navigateToLogoutUrl', async function (timeoutInMillis = 30000) {
    await page.goto(logoutUrl, { waitUntil: ['load', 'networkidle0'], timeout: timeoutInMillis });
  });
  await synthetics.executeStep('clickLogout', async function () {
    await page.waitForSelector('a[href="/logout"]', { timeout: 30000 });
    await page.click('a[href="/logout"]');
  });
};

exports.handler = async () => {
  return await flowBuilderBlueprint();
};

こちらのスクリプトをそのままアップグレードしてみます。

02-ランタイムを更新
03-ランタイム更新のポップアップ

ポップアップにもある通り、ランタイムのアップグレードを行う際はコピー(クローン)を作成してからアップグレードを行うことをおすすめします。

アップグレードし実行したところ今回のスクリプトでは以下2つのエラーが発生しました。

Error: Cannot find module 'aws-sdk'
TypeError: page.waitForXPath is not a function Stack: TypeError: page.waitForXPath is not a function

Error: Cannot find module 'aws-sdk'はSDK for JavaScript V2を利用していたために発生しているエラーです。こちらはSDK for JavaScript V3への移行を行うことで解消されます。

今回はスクリプトではv2で呼び出していたSecrets ManagerのモジュールをV3で呼び出すように変更しました。

> const { SecretsManagerClient, GetSecretValueCommand } = require("@aws-sdk/client-secrets-manager");
21c22,23
<   var AWS = require('aws-sdk');
---
>   const client = new SecretsManagerClient({ region: region });
>   const command = new GetSecretValueCommand({ SecretId: secretName });
23,33c25,35
<   const client = new AWS.SecretsManager({
<     region: region,
<   });
<
<   const secret = await client.getSecretValue({ SecretId: secretName }).promise();
<
<   if ('SecretString' in secret) {
<     const secretObj = JSON.parse(secret.SecretString);
<     basicpassword = secretObj.basicpassword;
<     cmppassword = secretObj.cmppassword;
<     cmpidpassword = secretObj.cmpidpassword;
---
>   try {
>     const response = await client.send(command);
>     if ('SecretString' in response) {
>       const secretObj = JSON.parse(response.SecretString);
>       basicpassword = secretObj.basicpassword;
>       cmppassword = secretObj.cmppassword;
>       cmpidpassword = secretObj.cmpidpassword;
>     }
>   } catch (error) {
>     console.error("Error retrieving secret:", error);
>     throw error;

TypeError: page.waitForXPath...はPuppeteer-coreのバージョンアップに伴う破壊的変更の「remove $x and waitForXpath (#11782)」によるものです。

こちらはwaitForXPathwaitForSelectorに変更するように修正します。

124c135,144
<     await page.waitForXPath("//h1[contains(text(), 'はじめに')]", { timeout: 30000 });
---
>     await page.waitForFunction((xpathExpression) => {
>       const element = document.evaluate(
>         xpathExpression,
>         document,
>         null,
>         XPathResult.FIRST_ORDERED_NODE_TYPE,
>         null
>       ).singleNodeValue;
>       return element !== null;
>     }, { timeout: 30000 }, "//h1[contains(text(), 'はじめに')]");

修正後のスクリプトはこちらです。

const synthetics = require('Synthetics');
const log = require('SyntheticsLogger');
const syntheticsConfiguration = synthetics.getConfiguration();
const { SecretsManagerClient, GetSecretValueCommand } = require("@aws-sdk/client-secrets-manager");

const flowBuilderBlueprint = async function () {
  // 環境変数の設定
  const url = process.env.URL;
  const region = process.env.REGION;
  const secretName = process.env.SECRETNAME;

  // Synthetics の設定
  syntheticsConfiguration.setConfig({
    includeRequestHeaders: true,
    includeResponseHeaders: true,
    restrictedHeaders: [],
    restrictedUrlParameters: [],
  });

  // AWS Secrets Manager からシークレットを取得
  const client = new SecretsManagerClient({ region: region });
  const command = new GetSecretValueCommand({ SecretId: secretName });

  try {
    const response = await client.send(command);
    if ('SecretString' in response) {
      const secretObj = JSON.parse(response.SecretString);
      basicpassword = secretObj.basicpassword;
      cmppassword = secretObj.cmppassword;
      cmpidpassword = secretObj.cmpidpassword;
    }
  } catch (error) {
    console.error("Error retrieving secret:", error);
    throw error;
  }

  let page = await synthetics.getPage();

  // Basic 認証の設定
  await page.setExtraHTTPHeaders({
    Authorization: `Basic ${new Buffer.from(`${basicUsername}:${basicPassword}`).toString('base64')}`,
  });

  // 初期URLへ遷移
  await synthetics.executeStep('navigateToUrl', async function (timeoutInMillis = 30000) {
    await page.goto(url, { waitUntil: ['load', 'networkidle0'], timeout: timeoutInMillis });
  });

  // ポータルのログインプロセス
  await synthetics.executeStep('inputPortalUsername', async function () {
    await page.type("[type='text']", portalUserId);
  });

  await synthetics.executeStep('inputPortalPassword', async function () {
    await page.type("[type='password']", portalPassword);
  });

  await synthetics.executeStep('submitPortalLogin', async function () {
    await page.waitForSelector("[type='submit']", { timeout: 30000 });
    await page.click("[type='submit']");
  });

  await synthetics.executeStep('verifyPortalLogin', async function () {
    await page.waitForFunction((xpathExpression) => {
      const element = document.evaluate(
        xpathExpression,
        document,
        null,
        XPathResult.FIRST_ORDERED_NODE_TYPE,
        null
      ).singleNodeValue;
      return element !== null;
    }, { timeout: 30000 }, "//h1[contains(text(), 'はじめに')]");
  });

  // ログアウトプロセス
  const logoutUrl = 'https://example.com/home';
  await synthetics.executeStep('navigateToLogoutUrl', async function (timeoutInMillis = 30000) {
    await page.goto(logoutUrl, { waitUntil: ['load', 'networkidle0'], timeout: timeoutInMillis });
  });
  await synthetics.executeStep('clickLogout', async function () {
    await page.waitForSelector('a[href="/logout"]', { timeout: 30000 });
    await page.click('a[href="/logout"]');
  });
};

exports.handler = async () => {
  return await flowBuilderBlueprint();
};

こちらの修正版スクリプトをsyn-nodejs-puppeteer-9.0で実行してみると正常に実行されました。

04-正常に実行した

依存関係を含むパッケージングしたZipファイルを用意しアップグレードする

次に依存関係を含むパッケージングしたZipファイルを用意しアップグレードを行う方法について説明します。

CloudWatch SyntheticsではS3に依存関係を含むパッケージングしたZipファイルを用意してCanaryとして利用することができます。

https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_WritingCanary_Nodejs.html#CloudWatch_Synthetics_Canaries_package

syn-nodejs-puppeteer-7.0で利用していたCanaryスクリプトをindex.jsとして用意し、依存関係を含むnode_modulesディレクトリを含むZipファイルを作成します。

以下のシェルスクリプトを実行し、index.jsとSDK for Javascript V2を含むnode_modulesディレクトリをZipファイルで作成します。

#!/bin/bash

mkdir temp_dir && cd temp_dir && \
npm init -y && \
npm install aws-sdk puppeteer-core@21.9.0 && \
mkdir -p nodejs/node_modules && \
cp ../index.js nodejs/ && \
cp -r node_modules/* nodejs/node_modules/ && \
zip -r ../canary.zip nodejs && \
cd .. && \
rm -rf temp_dir && \

こちらのZipファイルをS3にアップロードして、「S3からインポート」でCanaryを作成します。

05-S3からインポート

すると、以下のようなエラーが発生しました..

06-Lambdaエラーメッセージ

エラーメッセージ:Layers consume more than the available size of 262144000 bytes

こちらはLambdaレイヤーのデプロイパッケージサイズ(250 MB)のクォーター上限に引っかかっているエラーです。

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/gettingstarted-limits.html

CloudWatch Syntheticsは裏側ではLambdaが動いています。

CloudWatch Syntheticsが作成Lambdaレイヤーを確認すると、「Synthetics」が追加するレイヤーと「ユーザー」が追加するレイヤーを確認することができます。

07-Lambdaレイヤー

「Synthetics」が追加するレイヤーサイズを確認すると、解凍後「163.5MB」あることが確認できます。

Syntheticsレイヤーダウンロードスクリプト
aws lambda get-layer-version \
    --layer-name arn:aws:lambda:ap-northeast-1:172291836251:layer:Synthetics \
    --version-number 52 \
    --region ap-northeast-1 \
    --query 'Content.Location' \
    --output text | xargs curl -o Synthetics_layer_v52.zip

08-Synthetics-v52サイズ確認

今回作成したZipファイルの解凍後サイズは「126.1MB」のため、Syntheticsレイヤーと合わせて「289.6MB」となり、クォーターを超えてしまっていることがわかります。

09-zipファイルサイズ確認

そのため、パッケージサイズに収めるためにはaws-sdkpuppeteer-coreどちらかのモジュールの依存関係しか残すことができなさそうです。

ちなみに、確認時点でのaws-sdkとpuppeteer-coreのモジュールサイズは以下の通りです。

  • aws-sdk 2.1691.0: 103MB
  • puppeteer-core 21.9.0: 23.4MB

ちなみに、Syntheticsレイヤーとaws-sdkのサイズ合計で250MBを超えていますが、aws-sdkのみであればCanaryとして作成できることは確認できました。

ただ、Zipファイルから作成したCanaryは「統合されたマルチファイル編集は現在サポートされていません。」としてコンソールでの編集ができないのでスクリプト修正についてはローカル環境で実施し、再度Zipファイルとしてアップロードする必要がある点についてはご注意ください。

10-統合されたマルチファイル編集はサポートされない

最後に

今回はCloudWatch Syntheticsのsyn-nodejs-puppeteer-7.0からsyn-nodejs-puppeteer-9.0へのアップグレードについてご紹介しました。

基本的にはスクリプトを修正してアップグレードを行うのがおすすめですが、破壊的な変更による移行コストがかかる場合には依存関係を含むパッケージングしたZipファイルを用意してアップグレードを行う方法もあります。

スクリプトの改修またはZipファイルによる依存関係の継続を使ってSyntheticsのランタイムサポートに合わせて計画的にアップグレード対応を検討いただければと思います。

Synthetics のランタイムバージョン - Amazon CloudWatch

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.